Skip to content

feat(rapier-cluster): per-entity body kind / material / filtering / sensor hooks (#120)#125

Merged
martinjms merged 1 commit intofeat/rapier-clusterfrom
feat/120-per-entity-body-kind
May 4, 2026
Merged

feat(rapier-cluster): per-entity body kind / material / filtering / sensor hooks (#120)#125
martinjms merged 1 commit intofeat/rapier-clusterfrom
feat/120-per-entity-body-kind

Conversation

@martinjms
Copy link
Copy Markdown
Contributor

Quick Summary

  • Adds four per-entity spawn-time hooks to RapierClusterSimulation: body_kind_for, material_for, collision_groups_for, is_sensor.
  • Closes the gap in Rapier physics: per-entity body kind + material + collision filtering + sensor hooks #120 — every spawned entity is no longer forced to be a default-tuned Dynamic body.
  • All hooks have default impls (Dynamic / zero-friction / all-collide / non-sensor), so existing code keeps working unchanged.
  • New public types: RapierBodyKind, RapierMaterial, RapierCollisionGroups. Re-exports rapier3d::geometry::Group so users don't need a direct rapier3d dep.

Change Type

  • feature

Impact

  • User/developer impact: Game devs implementing RapierClusterSimulation can now declare per-entity body kind (Dynamic/Kinematic*/Fixed), material (friction/restitution/density), collision filtering (memberships/filter bitsets), and sensor flag. Default trait methods preserve the current "everything is Dynamic with default material" behavior — no breaking change for existing impls.
  • Risk level: Low. Lib code is internal to rapier_cluster; no hot-path or replication changes. Five spawn-time hooks behind default methods with unchanged invocation lifecycle.

Verification

  • Build passes (cargo build -p arcane-infra and --features rapier-cluster)
  • Tests pass (cargo test -p arcane-infra --features rapier-cluster --lib — 76 passing including 8 new Rapier physics: per-entity body kind + material + collision filtering + sensor hooks #120 tests)
  • Doc examples compile (cargo test --doc -p arcane-infra --features rapier-cluster)
  • Clippy clean both feature configs (cargo clippy --all-targets -- -D warnings)
  • Formatter clean (cargo fmt --all -- --check)

Decisions made

  • Bundle 5 spawn params into internal SpawnParams struct. Considered: 8-arg spawn() + #[allow(clippy::too_many_arguments)].
    Reason: Cleaner call site, gives one place to extend for future hooks (CCD toggle, etc.); clippy-clean without lint suppression.

  • pub const fn new(...) constructors on RapierMaterial and RapierCollisionGroups. Considered: builder pattern; struct-update with ..Default::default().
    Reason: #[non_exhaustive] blocks struct-literal construction outside the defining crate (E0639). new is the simplest external constructor and stays const. Doc example wouldn't compile without this.

  • Re-export rapier3d::geometry::Group. Considered: leave it; users add rapier3d as a direct dep.
    Reason: Constructing RapierCollisionGroups requires Group::GROUP_1 etc.; re-exporting keeps the rapier3d version pinning under arcane-infra's control and matches the existing pattern of hiding the rapier3d types behind crate-root re-exports.

  • RapierBodyKind derives Default. Considered: manual impl matching the issue spec wording.
    Reason: clippy derivable-impls flagged the manual version; the derive form (with #[default] on Dynamic) is idiomatic and shorter.

  • Restitution test tracks peak position only after tick 20. Considered: track peak across all ticks (initial draft).
    Reason: Pre-impact peak is just the drop position (y=3), so both elastic and inelastic runs returned ≈3 and the test was a tautology. Skipping the pre-impact window measures the actual rebound.

  • Single parameterized HookSim test fixture vs N narrow ones. Considered: per-test fixtures matching the existing ContactRecorder pattern.
    Reason: The 8 Rapier physics: per-entity body kind + material + collision filtering + sensor hooks #120 tests need heterogeneous per-entity configs (collision groups, sensor, body kind) plus per-(hook, entity) call-counting for the once-per-entity invariant. One fixture with EntitySpec overrides covers all of them; N narrow fixtures duplicate machinery.

  • Test gravity setup pattern. Considered: setting gravity globally for the test module.
    Reason: Most existing tests rely on the default zero-gravity for benchmark parity; selectively overriding gravity per-test (with the documented RapierConfig { gravity: [0, -9.81, 0], ..Default::default() } snippet from the issue body) keeps the rest of the test suite unchanged.

Implementation notes

Architecture fit

  • All four new hooks live on RapierClusterSimulation only — users on the plain ClusterSimulation path get all defaults (matches collider_for's existing behavior).
  • The SpawnParams struct is pub(crate) (private to the module). Public users still see five named hook methods, not a struct.
  • RapierBodyKind, RapierMaterial, RapierCollisionGroups are all #[non_exhaustive] for SemVer flexibility (per existing convention with RapierConfig / RapierColliderShape).

Scope-split call-out

Introducing Fixed here only changes physics-side behavior (solver-skipped, only AABB tracked in broadphase). Until the (unfiled) clustering-binding epic lands, Fixed entities still migrate by PGP affinity — they are not yet pinned to chunk ownership. The clustering layer is unchanged in this PR.

Module docs

  • New # Per-entity spawn-time hooks section enumerating the four hooks and their defaults.
  • New ## Subclass-style vs property-value-style sub-section per entity-model.md §5.
  • Extended # Example with a property-value-style MyGame impl that uses every hook.

Tests

8 new tests appended to mod tests. Shared fixture HookSim lets each test set per-entity overrides via EntitySpec and inspect contact events / hook call counts.

Reference

…ensor hooks (#120)

Add four spawn-time customization hooks to RapierClusterSimulation:
body_kind_for, material_for, collision_groups_for, is_sensor. Closes the
gap between "Rapier integration exists" and "Rapier integration can model
the entities a real game has."

New public types (all #[non_exhaustive]):
- RapierBodyKind { Dynamic | KinematicPositionBased | KinematicVelocityBased | Fixed }
- RapierMaterial { friction, restitution, density } + ::new() constructor
- RapierCollisionGroups { memberships, filter } + ::new() constructor

Re-exports rapier3d::geometry::Group from arcane_infra root so users can
construct collision groups without depending on rapier3d directly.

All five hooks (these four plus the existing collider_for) are called
exactly once per entity at first-sight spawn; subsequent return-value
changes are ignored for already-spawned bodies.

Scope-split: introducing the Fixed body-kind variant only changes
physics-side behavior (solver-skipped, AABB-tracked). Until the
clustering-binding epic lands, Fixed entities still migrate by PGP
affinity — they are not yet pinned to chunk ownership.

Tests (8 new):
- fixed_body_does_not_move_under_gravity
- kinematic_position_based_ignores_gravity
- material_for_is_honored_on_collider (structural)
- high_restitution_bounces_higher_than_zero_restitution (dynamic)
- density_changes_body_mass
- non_overlapping_collision_groups_filter_contacts
- sensor_fires_event_without_pushback
- all_hooks_called_exactly_once_per_entity

Verification: build clean, 46/46 rapier_cluster tests pass, doc examples
compile, clippy clean both feature configurations, fmt clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@martinjms
Copy link
Copy Markdown
Contributor Author

Self-review against #120 spec

Architecture fit

  • Stays inside arcane-infra::rapier_cluster. No touch to clustering / replication / hot-path.
  • Adds public API surface, but the surface (4 new types + 4 new trait methods) was approved at issue time after a design pass that aligned names against entity-model.md §4–§6.
  • Hook signatures mirror existing collider_for exactly. Same lifecycle (called once per entity at first-sight).
  • Default impls preserve current behavior — no breaking change for existing RapierClusterSimulation impls.

Spec compliance

  • RapierBodyKind variants match the canonical taxonomy (entity-model.md §4): Dynamic / KinematicPositionBased / KinematicVelocityBased / Fixed.
  • RapierMaterial::default() = zero-friction / zero-restitution / unit-density per spec.
  • RapierCollisionGroups::default() = Group::ALL / Group::ALL per spec.
  • All new types are #[non_exhaustive] per spec.
  • Spatial-binding scope-split is documented in module-level docs and on the RapierBodyKind doc — Fixed is physics-only here; clustering binding is a future epic.

Test coverage

  • 8/8 new tests pass; cover all four hooks plus the once-per-entity invariant.
  • Restitution test windowing fix (peak after tick 20) correctly distinguishes elastic vs inelastic — sanity check: bouncy peak >> dead peak by >1.0m.
  • Density test verifies analytical 2× ratio.
  • Sensor test verifies both halves: event fires, no pushback.

Minor observations (not blocking):

  • 💡 set_linvel is still called every tick on Fixed bodies. Rapier silently ignores this for Fixed bodies (no-op), so it's correct but slightly wasteful. Worth a follow-up tweak if profiling shows it matters; not worth blocking on now.
  • 💡 Doc example reads entry.user_data.get("kind") but doesn't show how user_data gets populated. Illustrative example, not a runnable end-to-end pattern. Acceptable for module docs.

CI:rust-checks SUCCESS.

Verdict: clean implementation, fully spec-compliant. Approving and squash-merging into feat/rapier-cluster.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant